package cn.jingyuan.owl.utils.core;

import cn.jingyuan.owl.utils.core.exceptions.UtilsException;
import cn.jingyuan.owl.utils.core.io.FileUtils;
import cn.jingyuan.owl.utils.core.io.IORuntimeException;
import cn.jingyuan.owl.utils.core.io.IoUtils;
import cn.jingyuan.owl.utils.core.io.resource.ResourceUtils;
import cn.jingyuan.owl.utils.core.lang.Assert;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.URLStreamHandler;
import java.nio.charset.Charset;
import java.util.jar.JarFile;

/**
 * 统一资源定位符相关工具类
 */
public class URLUtils {

    /** 针对 ClassPath 路径的伪协议前缀（兼容 Spring）: "classpath:" */
    public static final String CLASSPATH_URL_PREFIX = "classpath:";

    /** URL 前缀表示文件: "file:" */
    public static final String FILE_URL_PREFIX = "file:";

    /** URL 前缀表示 jar: "jar:" */
    public static final String JAR_URL_PREFIX = "jar:";

    /** URL 前缀表示 war: "war:" */
    public static final String WAR_URL_PREFIX = "war:";

    /** URL 协议表示文件: "file" */
    public static final String URL_PROTOCOL_FILE = "file";

    /** URL 协议表示 Jar 文件: "jar" */
    public static final String URL_PROTOCOL_JAR = "jar";

    /** URL 协议表示 zip 文件: "zip" */
    public static final String URL_PROTOCOL_ZIP = "zip";

    /** URL 协议表示 WebSphere 文件: "wsjar" */
    public static final String URL_PROTOCOL_WSJAR = "wsjar";

    /** URL 协议表示 JBoss zip 文件: "vfszip" */
    public static final String URL_PROTOCOL_VFSZIP = "vfszip";

    /** URL 协议表示 JBoss 文件: "vfsfile" */
    public static final String URL_PROTOCOL_VFSFILE = "vfsfile";

    /** URL 协议表示 JBoss VFS 资源: "vfs" */
    public static final String URL_PROTOCOL_VFS = "vfs";

    /** Jar 路径以及内部文件路径的分界符: "!/" */
    public static final String JAR_URL_SEPARATOR = "!/";

    /** WAR 路径及内部文件路径分界符 */
    public static final String WAR_URL_SEPARATOR = "*/";

    /**
     * 通过一个字符串形式的 URL 地址创建 URL 对象
     *
     * @param url URL
     *
     * @return URL 对象
     */
    public static URL url(String url) {
        return url(url, null);
    }

    /**
     * 通过一个字符串形式的 URL 地址创建 URL 对象
     *
     * @param url URL
     * @param handler {@link URLStreamHandler}
     *
     * @return URL 对象
     */
    public static URL url(String url, URLStreamHandler handler) {
        Assert.notNull(url, "URL must not be null");

        // 兼容 Spring 的 ClassPath 路径
        if (url.startsWith(CLASSPATH_URL_PREFIX)) {
            url = url.substring(CLASSPATH_URL_PREFIX.length());
            return ClassLoaderUtils.getClassLoader().getResource(url);
        }

        try {
            return new URL(null, url, handler);
        } catch (MalformedURLException e) {
            // 尝试文件路径
            try {
                return new File(url).toURI().toURL();
            } catch (MalformedURLException ex2) {
                throw new UtilsException(e);
            }
        }
    }

    /**
     * 将 URL 字符串转换为 URL 对象，并做必要验证
     *
     * @param urlString URL 字符串
     *
     * @return URL
     */
    public static URL toUrlForHttp(String urlString) {
        return toUrlForHttp(urlString, null);
    }

    /**
     * 将 URL 字符串转换为 URL 对象，并做必要验证
     *
     * @param urlString URL 字符串
     * @param handler {@link URLStreamHandler}
     *
     * @return URL
     */
    public static URL toUrlForHttp(String urlString, URLStreamHandler handler) {
        Assert.notBlank(urlString, "Url is blank !");
        // 编码空白符，防止空格引起的请求异常
        urlString = encodeBlank(urlString);
        try {
            return new URL(null, urlString, handler);
        } catch (MalformedURLException e) {
            throw new UtilsException(e);
        }
    }

    /**
     * 单独编码 URL 中的空白符，空白符编码为%20
     *
     * @param urlString URL 字符串
     *
     * @return 编码后的字符串
     */
    public static String encodeBlank(CharSequence urlString) {
        if (urlString == null) {
            return null;
        }

        int len = urlString.length();
        final StringBuilder sb = new StringBuilder(len);
        char c;
        for (int i = 0; i < len; i++) {
            c = urlString.charAt(i);
            if (CharUtils.isBlankChar(c)) {
                sb.append("%20");
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    /**
     * 获得 URL
     *
     * @param pathBaseClassLoader 相对路径（相对于 classes）
     *
     * @return URL
     *
     * @see ResourceUtils#getResource(String)
     */
    public static URL getURL(String pathBaseClassLoader) {
        return ResourceUtils.getResource(pathBaseClassLoader);
    }

    /**
     * 获得 URL
     *
     * @param path 相对给定 class 所在的路径
     * @param clazz 指定 class
     *
     * @return URL
     *
     * @see ResourceUtils#getResource(String, Class)
     */
    public static URL getURL(String path, Class<?> clazz) {
        return ResourceUtils.getResource(path, clazz);
    }

    /**
     * 获得 URL，常用于使用绝对路径时的情况
     *
     * @param file URL 对应的文件对象
     *
     * @return URL
     *
     * @throws UtilsException MalformedURLException
     */
    public static URL getURL(File file) {
        Assert.notNull(file, "File is null !");
        try {
            return file.toURI().toURL();
        } catch (MalformedURLException e) {
            throw new UtilsException(e, "Error occured when get URL!");
        }
    }

    /**
     * 获得 URL，常用于使用绝对路径时的情况
     *
     * @param files URL 对应的文件对象
     *
     * @return URL
     *
     * @throws UtilsException MalformedURLException
     */
    public static URL[] getURLs(File... files) {
        final URL[] urls = new URL[files.length];
        try {
            for (int i = 0; i < files.length; i++) {
                urls[i] = files[i].toURI().toURL();
            }
        } catch (MalformedURLException e) {
            throw new UtilsException(e, "Error occured when get URL!");
        }

        return urls;
    }

    /**
     * 获取 URL 中域名部分，只保留 URL 中的协议（Protocol）、Host，其它为 null。
     *
     * @param url URL
     *
     * @return 域名的 URI
     */
    public static URI getHost(URL url) {
        if (null == url) {
            return null;
        }

        try {
            return new URI(url.getProtocol(), url.getHost(), null, null);
        } catch (URISyntaxException e) {
            throw new UtilsException(e);
        }
    }

    /**
     * 补全相对路径
     *
     * @param baseUrl 基准 URL
     * @param relativePath 相对 URL
     *
     * @return 相对路径
     *
     * @throws UtilsException MalformedURLException
     */
    public static String completeUrl(String baseUrl, String relativePath) {
        baseUrl = normalize(baseUrl, false);
        if (StringUtils.isBlank(baseUrl)) {
            return null;
        }

        try {
            final URL absoluteUrl = new URL(baseUrl);
            final URL parseUrl = new URL(absoluteUrl, relativePath);
            return parseUrl.toString();
        } catch (MalformedURLException e) {
            throw new UtilsException(e);
        }
    }

    /**
     * 编码 URL，默认使用 UTF-8 编码<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。
     *
     * @param url URL
     *
     * @return 编码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String encodeAll(String url) {
        return encodeAll(url, CharsetUtils.CHARSET_UTF_8);
    }

    /**
     * 编码 URL<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。
     *
     * @param url URL
     * @param charset 编码
     *
     * @return 编码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String encodeAll(String url, Charset charset) throws UtilsException {
        try {
            return URLEncoder.encode(url, charset.toString());
        } catch (UnsupportedEncodingException e) {
            throw new UtilsException(e);
        }
    }

    /**
     * 编码 URL，默认使用 UTF-8 编码<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>
     * 此方法用于 URL 自动编码，类似于浏览器中键入地址自动编码，对于像类似于“/”的字符不再编码
     *
     * @param url URL
     *
     * @return 编码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String encode(String url) throws UtilsException {
        return encode(url, CharsetUtils.CHARSET_UTF_8);
    }

    /**
     * 编码 URL，默认使用 UTF-8 编码<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头<br>
     * 此方法用于 POST 请求中的请求体自动编码，转义大部分特殊字符
     *
     * @param url URL
     *
     * @return 编码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String encodeQuery(String url) throws UtilsException {
        return encodeQuery(url, CharsetUtils.CHARSET_UTF_8);
    }

    /**
     * 编码字符为 application/x-www-form-urlencoded<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>
     * 此方法用于 URL 自动编码，类似于浏览器中键入地址自动编码，对于像类似于“/”的字符不再编码
     *
     * @param url 被编码内容
     * @param charset 编码
     *
     * @return 编码后的字符
     */
    public static String encode(String url, Charset charset) {
        if (StringUtils.isEmpty(url)) {
            return url;
        }
        if (null == charset) {
            charset = CharsetUtils.defaultCharset();
        }
        return cn.jingyuan.owl.utils.core.net.URLEncoder.DEFAULT.encode(url, charset);
    }

    /**
     * 编码字符为 URL 中查询语句<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>
     * 此方法用于 POST 请求中的请求体自动编码，转义大部分特殊字符
     *
     * @param url 被编码内容
     * @param charset 编码
     *
     * @return 编码后的字符
     */
    public static String encodeQuery(String url, Charset charset) {
        if (StringUtils.isEmpty(url)) {
            return url;
        }
        if (null == charset) {
            charset = CharsetUtils.defaultCharset();
        }
        return cn.jingyuan.owl.utils.core.net.URLEncoder.QUERY.encode(url, charset);
    }

    /**
     * 编码 URL 字符为 application/x-www-form-urlencoded<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>
     * 此方法用于 URL 自动编码，类似于浏览器中键入地址自动编码，对于像类似于“/”的字符不再编码
     *
     * @param url URL
     * @param charset 编码
     *
     * @return 编码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String encode(String url, String charset) throws UtilsException {
        if (StringUtils.isEmpty(url)) {
            return url;
        }
        return encode(url, StringUtils.isBlank(charset) ? CharsetUtils.defaultCharset() : CharsetUtils.charset(charset));
    }

    /**
     * 编码 URL<br>
     * 将需要转换的内容（ASCII 码形式之外的内容），用十六进制表示法转换出来，并在之前加上%开头。<br>
     * 此方法用于 POST 请求中的请求体自动编码，转义大部分特殊字符
     *
     * @param url URL
     * @param charset 编码
     *
     * @return 编码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String encodeQuery(String url, String charset) throws UtilsException {
        return encodeQuery(url, StringUtils.isBlank(charset) ? CharsetUtils.defaultCharset() : CharsetUtils.charset(charset));
    }

    /**
     * 解码 URL<br>
     * 将%开头的 16 进制表示的内容解码。
     *
     * @param url URL
     *
     * @return 解码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String decode(String url) throws UtilsException {
        return decode(url, CharsetUtils.UTF_8);
    }

    /**
     * 解码 application/x-www-form-urlencoded 字符
     *
     * @param content 被解码内容
     * @param charset 编码
     *
     * @return 编码后的字符
     */
    public static String decode(String content, Charset charset) {
        if (null == charset) {
            charset = CharsetUtils.defaultCharset();
        }
        return decode(content, charset.name());
    }

    /**
     * 解码 URL<br>
     * 将%开头的 16 进制表示的内容解码。
     *
     * @param url URL
     * @param charset 编码
     *
     * @return 解码后的 URL
     *
     * @throws UtilsException UnsupportedEncodingException
     */
    public static String decode(String url, String charset) throws UtilsException {
        if (StringUtils.isEmpty(url)) {
            return url;
        }
        try {
            return URLDecoder.decode(url, charset);
        } catch (UnsupportedEncodingException e) {
            throw new UtilsException(e, "Unsupported encoding: [{}]", charset);
        }
    }

    /**
     * 获得 path 部分<br>
     *
     * @param urlString URI 路径
     *
     * @return path
     *
     * @throws UtilsException 包装 URISyntaxException
     */
    public static String getPath(String urlString) {
        URI uri;
        try {
            uri = new URI(urlString);
        } catch (URISyntaxException e) {
            throw new UtilsException(e);
        }
        return uri.getPath();
    }

    /**
     * 从 URL 对象中获取不被编码的路径 Path<br>
     * 对于本地路径，URL 对象的 getPath 方法对于包含中文或空格时会被编码，导致本读路径读取错误。<br>
     * 此方法将 URL 转为 URI 后获取路径用于解决路径被编码的问题
     *
     * @param url {@link URL}
     *
     * @return 路径
     */
    public static String getDecodedPath(URL url) {
        if (null == url) {
            return null;
        }

        String path = null;
        try {
            // URL 对象的 getPath 方法对于包含中文或空格的问题
            path = URLUtils.toURI(url).getPath();
        } catch (UtilsException e) {
            // ignore
        }
        return (null != path) ? path : url.getPath();
    }

    /**
     * 转 URL 为 URI
     *
     * @param url URL
     *
     * @return URI
     *
     * @throws UtilsException 包装 URISyntaxException
     */
    public static URI toURI(URL url) throws UtilsException {
        return toURI(url, false);
    }

    /**
     * 转 URL 为 URI
     *
     * @param url URL
     * @param isEncode 是否编码参数中的特殊字符（默认 UTF-8 编码）
     *
     * @return URI
     *
     * @throws UtilsException 包装 URISyntaxException
     */
    public static URI toURI(URL url, boolean isEncode) throws UtilsException {
        if (null == url) {
            return null;
        }

        return toURI(url.toString(), isEncode);
    }

    /**
     * 转字符串为 URI
     *
     * @param location 字符串路径
     *
     * @return URI
     *
     * @throws UtilsException 包装 URISyntaxException
     */
    public static URI toURI(String location) throws UtilsException {
        return toURI(location, false);
    }

    /**
     * 转字符串为 URI
     *
     * @param location 字符串路径
     * @param isEncode 是否编码参数中的特殊字符（默认 UTF-8 编码）
     *
     * @return URI
     *
     * @throws UtilsException 包装 URISyntaxException
     */
    public static URI toURI(String location, boolean isEncode) throws UtilsException {
        if (isEncode) {
            location = encode(location);
        }
        try {
            return new URI(location);
        } catch (URISyntaxException e) {
            throw new UtilsException(e);
        }
    }

    /**
     * 提供的 URL 是否为文件<br>
     * 文件协议包括"file", "vfsfile" 或 "vfs".
     *
     * @param url {@link URL}
     *
     * @return 是否为文件
     */
    public static boolean isFileURL(URL url) {
        String protocol = url.getProtocol();
        return (URL_PROTOCOL_FILE.equals(protocol) || //
            URL_PROTOCOL_VFSFILE.equals(protocol) || //
            URL_PROTOCOL_VFS.equals(protocol));
    }

    /**
     * 提供的 URL 是否为 jar 包 URL 协议包括： "jar", "zip", "vfszip" 或 "wsjar".
     *
     * @param url {@link URL}
     *
     * @return 是否为 jar 包 URL
     */
    public static boolean isJarURL(URL url) {
        final String protocol = url.getProtocol();
        return (URL_PROTOCOL_JAR.equals(protocol) || //
            URL_PROTOCOL_ZIP.equals(protocol) || //
            URL_PROTOCOL_VFSZIP.equals(protocol) || //
            URL_PROTOCOL_WSJAR.equals(protocol));
    }

    /**
     * 提供的 URL 是否为 Jar 文件 URL 判断依据为 file 协议且扩展名为.jar
     *
     * @param url the URL to check
     *
     * @return whether the URL has been identified as a JAR file URL
     */
    public static boolean isJarFileURL(URL url) {
        return (URL_PROTOCOL_FILE.equals(url.getProtocol()) && //
            url.getPath().toLowerCase().endsWith(FileUtils.JAR_FILE_EXT));
    }

    /**
     * 从 URL 中获取流
     *
     * @param url {@link URL}
     *
     * @return InputStream 流
     */
    public static InputStream getStream(URL url) {
        Assert.notNull(url);
        try {
            return url.openStream();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * 获得 Reader
     *
     * @param url {@link URL}
     * @param charset 编码
     *
     * @return {@link BufferedReader}
     */
    public static BufferedReader getReader(URL url, Charset charset) {
        return IoUtils.getReader(getStream(url), charset);
    }

    /**
     * 从 URL 中获取 JarFile
     *
     * @param url URL
     *
     * @return JarFile
     */
    public static JarFile getJarFile(URL url) {
        try {
            JarURLConnection urlConnection = (JarURLConnection) url.openConnection();
            return urlConnection.getJarFile();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * 标准化 URL 字符串，包括：
     *
     * <pre>
     * 1. 多个/替换为一个
     * </pre>
     *
     * @param url URL 字符串
     *
     * @return 标准化后的 URL 字符串
     */
    public static String normalize(String url) {
        return normalize(url, false);
    }

    /**
     * 标准化 URL 字符串，包括：
     *
     * <pre>
     * 1. 多个/替换为一个
     * </pre>
     *
     * @param url URL 字符串
     * @param isEncodePath 是否对 URL 中 path 部分的中文和特殊字符做转义（不包括 http:, /和域名部分）
     *
     * @return 标准化后的 URL 字符串
     */
    public static String normalize(String url, boolean isEncodePath) {
        if (StringUtils.isBlank(url)) {
            return url;
        }
        final int sepIndex = url.indexOf("://");
        String protocol;
        String body;
        if (sepIndex > 0) {
            protocol = StringUtils.subPre(url, sepIndex + 3);
            body = StringUtils.subSuf(url, sepIndex + 3);
        } else {
            protocol = "http://";
            body = url;
        }

        final int paramsSepIndex = StringUtils.indexOf(body, '?');
        String params = null;
        if (paramsSepIndex > 0) {
            params = StringUtils.subSuf(body, paramsSepIndex);
            body = StringUtils.subPre(body, paramsSepIndex);
        }

        if (StringUtils.isNotEmpty(body)) {
            // 去除开头的\或者/
            //noinspection ConstantConditions
            body = body.replaceAll("^[\\\\/]+", StringUtils.EMPTY);
            // 替换多个\或/为单个/
            body = body.replace("\\", "/").replaceAll("//+", "/");
        }

        final int pathSepIndex = StringUtils.indexOf(body, '/');
        String domain = body;
        String path = null;
        if (pathSepIndex > 0) {
            domain = StringUtils.subPre(body, pathSepIndex);
            path = StringUtils.subSuf(body, pathSepIndex);
        }
        if (isEncodePath) {
            path = encode(path);
        }
        return protocol + domain + StringUtils.nullToEmpty(path) + StringUtils.nullToEmpty(params);
    }

}